package com.grm.distributed.lock.service;

import com.grm.distributed.lock.common.Result;
import com.grm.distributed.lock.mapper.ProductMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * redis基于setnx指令实现分布式锁-重试（循环获取） --- 加入UUID防止锁误删
 *
 * 设置过期时间，防止死锁发生(但是也有缺陷，直接过期释放锁有可能让其他请求裸奔)
 *      通过UUID防止误删（恶意/无意误删锁） --- lock key value   (key一致，都是lock，那就给value加UUID)
 *      锁自动续期（业务还没执行完/程序宕机，锁的时间就到了）
 *      （这两者不相互矛盾）
 *
 * 给lock锁设置过期时间3s
 *
 * 原子性：
 *      判断锁和过期时间之间（set key value ex 20 nx 指令防止）
 *      判断和释放锁之间（lua脚本，下一案例中体现）
 *
 * ####(加锁+设置过期时间 没有保证原子性，不能这么操作)
 * stringRedisTemplate.expire("lock",3, TimeUnit.SECONDS);
 *
 * // 正确做法，使用set指令直接设置lock过期时间
 * set key value ex 20 nx
 * set lock 111 ex 20 nx
 *
 *
 *
 */
@Slf4j
@Service
public class ProductService8 {
    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public Result sell() {
        // 1. UUID防误删
        String uuid = UUID.randomUUID().toString();
        // 1. 执行setnx加锁,setIfPresent：存在就设置，setIfAbsent：不存在就设置(setnx),设置过期时间
        // 2. 获取锁失败，重试:循环重试
        while (!stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,3,TimeUnit.SECONDS)) {
            try {
                Thread.sleep(30);
                log.info("循环重试获取锁中：{}",uuid);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        try {
            // 3. 查库存
            String productCountStr = stringRedisTemplate.opsForValue().get("product_count");
            Integer count = Integer.valueOf(productCountStr);
            // 4. 扣减库存
            if (count > 0) {
                count = count - 1;
            }
            log.info("product current count = {}", count);
            stringRedisTemplate.opsForValue().set("product_count", String.valueOf(count));
        } finally {
            // 5. 解锁，UUID防误删
            String lockValue = stringRedisTemplate.opsForValue().get("lock");
            if(uuid.equals(lockValue)){
                stringRedisTemplate.delete("lock");
            }
        }
        return Result.success();
    }
}
